/*
 * uart suspend and resume for serial port
 *
 * Copyright (c) Huawei Technologies Co., Ltd. 2019-2019.
 * Description: add uart suspend and resume for serial port
 * Author: zhangwen
 * Create: 2019-06-11
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 */

#include <linux/moduleparam.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/types.h>
#include <linux/ctype.h>
#include <linux/serial_core.h>

#include <linux/hal/uart_suspend_drv.h>

/* name size which is used for tty name */
#define TTY_NAME_SIZE  128

atomic_t g_uart_suspend;

struct uart_suspend_ops serial_suspend_ops;
EXPORT_SYMBOL(serial_suspend_ops);

long tty_uart_suspend;
EXPORT_SYMBOL(tty_uart_suspend);

long tty_suspend_index = -1;
EXPORT_SYMBOL(tty_suspend_index);

static long tty_suspend_mode;
module_param_named(tty_suspend_mode, tty_suspend_mode, long, 0444);
MODULE_PARM_DESC(tty_suspend_mode, "tty suspend mode");

static char *tty_suspend_name;
module_param_named(tty_suspend_name, tty_suspend_name, charp, 0444);
MODULE_PARM_DESC(tty_suspend_name, "tty suspend name");

static const struct uart_ops *serial_get_pops(void)
{
	if (!is_uart_suspend_initialized())
		return NULL;
	return serial_suspend_ops.serial_get_pops();
}

struct uart_port *get_console_port(void)
{
	if (!is_uart_suspend_initialized())
		return NULL;
	return serial_suspend_ops.serial_get_console_port();
}

static struct uart_driver *get_serial_drv(void)
{
	if (!is_uart_suspend_initialized())
		return NULL;
	return serial_suspend_ops.serial_get_drv();
}

static int uart_startup_in_suspend(struct uart_port *port)
{
	return 0;
}

static void uart_shutdown_in_suspend(struct uart_port *port)
{
}

static void uart_enable_ms_in_suspend(struct uart_port *port)
{
}

static void uart_set_termios_in_suspend(struct uart_port *port, struct ktermios *termios,
					struct ktermios *old)
{
}

void uart_clear_xmit_ring(struct uart_port *port)
{
	struct uart_port *console_port = get_console_port();
	struct circ_buf *xmit = &port->state->xmit;
	unsigned int pending_size = 0;

	if (console_port != port)
		return;

	if (port->x_char) {
		port->icount.tx++;
		port->x_char = 0;
		return;
	}

	pending_size = uart_circ_chars_pending(xmit);
	xmit->tail = (int)(((unsigned int)xmit->tail + pending_size) & (UART_XMIT_SIZE - 1));
	port->icount.tx += pending_size;
	uart_write_wakeup(port);
}
EXPORT_SYMBOL(uart_clear_xmit_ring);

static void uart_start_tx_in_suspend(struct uart_port *port)
{
	return uart_clear_xmit_ring(port);
}

static struct uart_ops serial_pops_suspend;

static void recovery_ops_suspend(void)
{
	const struct uart_ops *pops = serial_get_pops();

	if (!pops) {
		printk(KERN_WARNING "no serial pops registered!\n");
		return;
	}

	memcpy((void *)&serial_pops_suspend, (const void *)pops, sizeof(struct uart_ops));
}

void update_uart_ops(void)
{
	struct uart_port *port = get_console_port();

	if (!port) {
		printk(KERN_WARNING "no tty console registered!\n");
		return;
	}

	if (port->ops == &serial_pops_suspend)
		return;

	serial_pops_suspend.startup = uart_startup_in_suspend;
	serial_pops_suspend.shutdown = uart_shutdown_in_suspend;
	serial_pops_suspend.enable_ms = uart_enable_ms_in_suspend;
	serial_pops_suspend.set_termios = uart_set_termios_in_suspend;
	serial_pops_suspend.start_tx = uart_start_tx_in_suspend;

	port->ops = &serial_pops_suspend;

	printk(KERN_WARNING"%s-%d\n", __func__, __LINE__);

	if (port->state->port.tty)
		uart_write_wakeup(port);
}

void recovery_uart_ops(void)
{
	struct uart_port *port = get_console_port();
	const struct uart_ops *pops = serial_get_pops();

	if (!port) {
		printk(KERN_WARNING "no tty console registered!\n");
		return;
	}

	if (port->ops == pops)
		return;

	port->ops = pops;
	recovery_ops_suspend();
}

bool is_rx_suspend(struct uart_port *port)
{
	struct uart_port *console_port = get_console_port();

	if ((console_port == port) &&
		(atomic_read(&g_uart_suspend) == UART_SUSPEND_RX || atomic_read(&g_uart_suspend) == UART_SW_SUSPEND)) {
		return true;
	} else {
		return false;
	}
}
EXPORT_SYMBOL(is_rx_suspend);

bool is_tx_suspend(struct uart_port *port)
{
	struct uart_port *console_port = get_console_port();

	if ((console_port == port) &&
		(atomic_read(&g_uart_suspend) == UART_SUSPEND_TX ||
		atomic_read(&g_uart_suspend) == UART_SW_SUSPEND))
		return true;
	else
		return false;
}
EXPORT_SYMBOL(is_tx_suspend);

void suspend_rx(void)
{
	int suspend_status = atomic_read(&g_uart_suspend);

	if (suspend_status != UART_RESUME &&
		suspend_status != UART_SUSPEND_TX) {
		if (suspend_status == UART_SUSPEND_RX)
			printk(KERN_ERR"the uart rx has been suspended!\n");
		else
			printk(KERN_ERR "Please resume_%s firstly\n", suspend_status == UART_SUSPEND?"uart":"tx");
		return;
	}

	atomic_set(&g_uart_suspend,
		suspend_status == UART_RESUME?UART_SUSPEND_RX:UART_SW_SUSPEND);
}
EXPORT_SYMBOL(suspend_rx);

void resume_rx(void)
{
	int suspend_status = atomic_read(&g_uart_suspend);

	if (suspend_status != UART_SUSPEND_RX &&
		suspend_status != UART_SW_SUSPEND) {
		if (suspend_status == UART_RESUME)
			printk(KERN_ERR"the uart is RESUME!\n");
		else
			printk(KERN_ERR"Please resume_%s firstly\n", suspend_status == UART_SUSPEND?"uart":"tx");
		return;
	}

	atomic_set(&g_uart_suspend,
		suspend_status == UART_SUSPEND_RX?UART_RESUME:UART_SUSPEND_TX);
}
EXPORT_SYMBOL(resume_rx);

void suspend_tx(void)
{
	int suspend_status = atomic_read(&g_uart_suspend);

	if (suspend_status != UART_RESUME &&
		suspend_status != UART_SUSPEND_RX) {
		if (suspend_status == UART_SUSPEND_TX)
			printk(KERN_ERR"the uart tx has been suspended!\n");
		else
			printk(KERN_ERR "Please resume_%s firstly\n", suspend_status == UART_SUSPEND?"uart":"rx");
		return;
	}

	atomic_set(&g_uart_suspend,
		suspend_status == UART_RESUME?UART_SUSPEND_TX:UART_SW_SUSPEND);
}
EXPORT_SYMBOL(suspend_tx);

void resume_tx(void)
{
	int suspend_status = atomic_read(&g_uart_suspend);

	if (suspend_status != UART_SUSPEND_TX &&
		suspend_status != UART_SW_SUSPEND) {
		if (suspend_status == UART_RESUME)
			printk(KERN_ERR"the uart is RESUME!\n");
		else
			printk(KERN_ERR"Please resume_%s firstly\n", suspend_status == UART_SUSPEND?"uart":"rx");
		return;
	}

	atomic_set(&g_uart_suspend,
		suspend_status == UART_SUSPEND_TX?UART_RESUME:UART_SUSPEND_RX);
}
EXPORT_SYMBOL(resume_tx);

void suspend_uart(void)
{
	int suspend_status = atomic_read(&g_uart_suspend);
	struct uart_driver *serial_drv = get_serial_drv();
	struct uart_port *port = get_console_port();

	if (!port) {
		printk(KERN_WARNING "suspend_uart: no tty console registered!\n");
		return;
	}

	if (!serial_drv) {
		printk(KERN_WARNING "suspend_uart: no serial drivers registered!\n");
		return;
	}

	if (suspend_status != UART_RESUME) {
		if (suspend_status == UART_SUSPEND)
			printk(KERN_ERR"the uart has been suspended!\n");
		else if (suspend_status == UART_SW_SUSPEND)
			printk(KERN_ERR"the uart has been SOFTWARE suspended! Pls resume_rx & tx\n");
		else
			printk(KERN_ERR "Please resume_%s firstly\n", suspend_status == UART_SUSPEND_TX?"tx":"rx");
		return;
	}

	atomic_set(&g_uart_suspend, UART_SUSPEND);
	printk(KERN_WARNING"%s-%d\n", __func__, __LINE__);
	uart_suspend_port(serial_drv, port);
	printk(KERN_WARNING"%s-%d: uart_suspend_port success\n", __func__, __LINE__);
}
EXPORT_SYMBOL(suspend_uart);

void resume_uart(void)
{
	int suspend_status = atomic_read(&g_uart_suspend);
	struct uart_driver *serial_drv = get_serial_drv();
	struct uart_port *port = get_console_port();

	if (!port) {
		printk(KERN_WARNING "resume_uart: no tty console registered!\n");
		return;
	}

	if (!serial_drv) {
		printk(KERN_WARNING "resume_uart: no serial drivers registered!\n");
		return;
	}

	if (suspend_status != UART_SUSPEND) {
		if (suspend_status == UART_RESUME)
			printk(KERN_ERR"the uart is RESUME!\n");
		else if (suspend_status == UART_SW_SUSPEND)
			printk(KERN_ERR"Please resume tx & rx firstly\n");
		else
			printk(KERN_ERR"Please resume_%s firstly\n", suspend_status == UART_SUSPEND_TX?"tx":"rx");
		return;
	}

	uart_resume_port(serial_drv, port);
	atomic_set(&g_uart_suspend, UART_RESUME);
}
EXPORT_SYMBOL(resume_uart);

void rm_uart(void)
{
	suspend_uart();
}
EXPORT_SYMBOL(rm_uart);

int is_uart_suspend(void)
{
	return atomic_read(&g_uart_suspend);
}
EXPORT_SYMBOL(is_uart_suspend);

bool is_uart_suspend_initialized(void)
{
	if (!serial_suspend_ops.serial_get_pops || \
		!serial_suspend_ops.serial_get_console_port || \
		!serial_suspend_ops.serial_get_drv)
		return false;
	return true;
}
EXPORT_SYMBOL(is_uart_suspend_initialized);

#ifndef CONFIG_SERIAL_CORE_MODULE
static int __init tty_suspend_mode_setup(char *str)
{
	int mode;

	if (get_option(&str, &mode)) {
		tty_suspend_mode = mode;
		return 0;
	}

	return -EINVAL;
}
__setup("tty_suspend_mode=", tty_suspend_mode_setup);
#endif

static void get_tty_suspend_info(void)
{
	char tty_name[TTY_NAME_SIZE] = {0};
	unsigned char i = 0;
	bool flag = false;
	char *s = tty_suspend_name;
	struct uart_driver *drv = NULL;

	if (tty_suspend_mode != 0)
		tty_uart_suspend = tty_suspend_mode;

	pr_info("get_tty_suspend_info: mode is %ld\n", tty_suspend_mode);

	if (s == NULL)
		return;

	for (; *s; s++) {
		if (i >= TTY_NAME_SIZE)
			break;

		if (isdigit(*s)) {
			flag = true;
			break;
		}
		tty_name[i++] = *s;
	}
	if (!flag) {
		pr_warn("get_tty_suspend_info: invaild tty suspend name!\n");
		return;
	}

	drv = get_serial_drv();
	if (!drv) {
		pr_warn("get_tty_suspend_info: no serial drivers registered!\n");
		return;
	}

	if (strcmp(drv->dev_name, tty_name))
		return;

	tty_suspend_index = simple_strtoul(s, NULL, 10);
	pr_info("get_tty_suspend_info: index is %ld\n", tty_suspend_index);
}

void uart_suspend_init(void)
{
	get_tty_suspend_info();

	atomic_set(&g_uart_suspend, tty_uart_suspend);

	recovery_ops_suspend();
}
EXPORT_SYMBOL(uart_suspend_init);

MODULE_LICENSE("GPL");
